package com.bkhech.home.practice.lottery;

import com.bkhech.home.practice.lottery.dto.Box;
import com.bkhech.home.practice.lottery.dto.BoxAward;
import com.bkhech.home.practice.lottery.dto.UserAward;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * DrawAwardService
 * 抽奖服务
 *
 * 该抽奖算法的优点主要包括以下几点：
 * 1.直观易懂：算法通过构建概率区间的方式，直观地反映了每个奖项的中奖概率，易于理解和实现。开发者能够方便地调整各个奖项的中奖概率，只需设定每个奖项的库存即可代表其被抽中的相对概率。
 * 2.效率较高：相较于每次抽奖都重新计算总体概率分布，该算法预先计算了概率区间，抽奖时仅需生成一个随机数并判断其落入哪个区间，减少了每次抽奖时的计算量，提高了效率。
 * 3.适应性强：可以根据活动需求灵活调整奖项和对应的概率，无论是少量高价值奖品还是大量低价值奖品，都能通过调整库存来实现期望的中奖概率分布。
 * 4.保证公平性：只要确保随机数生成的公平性，每个奖项被抽中的实际概率将接近其理论概率，从而保障了抽奖活动的基本公平原则。
 * 综上所述，此算法在实现抽奖功能时，以其实现的简便性、较高的执行效率及良好的适应性，成为了一种较为推荐的解决方案。
 *
 * 缺点：
 * 1. 超卖问题：仅仅靠该算法无法保证奖品超卖的情况，在保存数据时，需要额外的机制来防止超卖。
 *  比如：update box_award set stock = stock - #{num} where box_id = #{boxId} and award_id = #{setAwardId} and stock - #{num} >= 0; 可防止超卖
 * 2. 公平性问题：使用Random类生成随机数的方式可能受到系统时钟种子的影响，虽然对于大多数应用场景是足够的，但在追求高度公平性的抽奖场景下，这种随机方式可能被质疑不够随机，尤其是当抽奖结果需要 高度透明 和 可验证 时。
 *
 * {@literal https://blog.csdn.net/xiaojia1001/article/details/132914175}
 * {@literal https://www.cnblogs.com/sunshq/p/4171468.html}
 * @author guowm
 * @date 2024/06/26
 */
class DrawAwardService {

    /**
     * 根据抽奖次数从箱子中抽奖，并返回中奖奖品列表
     *
     * @param box     抽奖箱对象，包含所有奖品信息和库存情况
     * @param drawNum 抽奖次数
     * @return 返回一个用户中奖列表，列表中的每个元素代表一个中奖者及其所中的奖品
     */
    List<UserAward> drawAward(Box box, int drawNum) {
        // 获取箱子中的所有奖品信息
        final List<BoxAward> boxAwards = box.getBoxAwards();
        // 获取箱子的总库存数量
        final int boxStock = box.getStock();
        final int boxId = box.getId();

        if (boxStock == 0 || boxStock < drawNum) {
            throw new RuntimeException("抽奖箱库存不足");
        }

        // 初始化用户中奖列表，大小为抽奖次数
        List<UserAward> userAwards = new ArrayList<>(drawNum);
        // 初始化随机数生成器（他是一个伪随机数，默认种子是当前时间纳秒值）
        Random random = new Random();

        for (int i = 0; i < drawNum; i++) {
            // 在剩余的库存范围内随机一个数
            int r = random.nextInt(boxStock - i);
            // 上一次比对的累计库存数量
            int lastTotal = 0;

            // 遍历所有奖品，尝试匹配随机数 r
            for (int j = 0; j < boxAwards.size(); j++) {
                // 获取当前遍历到的奖品
                BoxAward boxAward = boxAwards.get(j);
                // 当前奖品的库存数量
                final int awardStock = boxAward.getStock();

                /*
                 * 将 r 与每个奖项的权重（库存）做减法，当 r 第一次小于0时，就表示n落在这个奖项的权重（库存）区间内，此用户中奖
                 */
                // 将 r 与每个奖项的权重（库存）做减法
                r -= awardStock;
                // 当 r 第一次小于0时，就表示n落在这个奖项的权重（库存）区间内，此用户中奖
                if (r < 0) {
                    // 中奖：
                    // 减少一个当前奖品的库存。因为每次抽取一个奖品
                    boxAward.setStock(awardStock - 1);

                    // 创建用户中奖记录对象
                    UserAward userAward = new UserAward();
                    userAward.setBoxId(boxId);
                    userAward.setGoodId(boxAward.getGoodId());
                    // 将中奖记录添加到用户中奖列表
                    userAwards.add(userAward);
                    // 结束当前奖品的匹配
                    break;
                } else {
                    // 未中奖：
                }
            }
        }
        // 返回用户中奖列表
        return userAwards;
    }

}